/**
 * 
 */
package edu.ubc.javascript;

import static edu.ubc.javascript.ASTFactory.assign;
import static edu.ubc.javascript.ASTFactory.comma;
import static edu.ubc.javascript.ASTFactory.exprStmt;
import static edu.ubc.javascript.ASTFactory.name;
import static edu.ubc.javascript.ASTFactory.paren;
import static edu.ubc.javascript.ASTFactory.prop;
import static edu.ubc.javascript.ContextCheck.isVariableReferenceName;

import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.Stack;

import com.google.javascript.jscomp.mozilla.rhino.Node;
import com.google.javascript.jscomp.mozilla.rhino.Token;
import com.google.javascript.jscomp.mozilla.rhino.ast.Assignment;
import com.google.javascript.jscomp.mozilla.rhino.ast.AstNode;
import com.google.javascript.jscomp.mozilla.rhino.ast.AstRoot;
import com.google.javascript.jscomp.mozilla.rhino.ast.Block;
import com.google.javascript.jscomp.mozilla.rhino.ast.ExpressionStatement;
import com.google.javascript.jscomp.mozilla.rhino.ast.FunctionCall;
import com.google.javascript.jscomp.mozilla.rhino.ast.FunctionNode;
import com.google.javascript.jscomp.mozilla.rhino.ast.InfixExpression;
import com.google.javascript.jscomp.mozilla.rhino.ast.Name;
import com.google.javascript.jscomp.mozilla.rhino.ast.NodeVisitor;
import com.google.javascript.jscomp.mozilla.rhino.ast.ObjectLiteral;
import com.google.javascript.jscomp.mozilla.rhino.ast.ObjectProperty;
import com.google.javascript.jscomp.mozilla.rhino.ast.ReturnStatement;
import com.google.javascript.jscomp.mozilla.rhino.ast.UnaryExpression;
import com.google.javascript.jscomp.mozilla.rhino.ast.VariableDeclaration;

public class ReadClosuresVisitor implements NodeVisitor {

	private Stack<String> fnNames = new Stack<String>();
	private Stack<String> scopeNames = new Stack<String>();
	private Stack<FunctionNode> funs = new Stack<FunctionNode>();
	private int count = 0;
	private Set<String> forcedClosed = new HashSet<String>();
	private final static String PARENT_SCOPE_NAME = Config.config().getProperty("functionClosureParentScopeName");
	private final static String SCOPE_NAME = Config.config().getProperty("functionClosurePropertyName");
	private static int fnCount = 0;
	private static final String TEMP_NAME = Config.config().getProperty("temporaryVariablePrefix");
	private static final String FUNCTION_PREFIX = Config.config().getProperty("functionRenamePrefix");
	public List<FunctionNode> innerFunctions = new LinkedList<FunctionNode>();
	private ReflectiveAstTransformer tx;
	private FunctionNodeClosureInfoVisitor fnciv;
	
	public ReadClosuresVisitor(FunctionNodeClosureInfoVisitor fnciv, ReflectiveAstTransformer tx) {
		this.fnciv = fnciv;
		this.tx = tx;
	}
	
	public static void reset() {
		fnCount = 0;
	}
	
	@Override
	public boolean visit(AstNode astNode) {
		try {
			if(astNode instanceof FunctionNode) {
				return handleFunctionNode((FunctionNode) astNode);
			} else if(astNode instanceof ReturnStatement) {
				return handleReturnStatement((ReturnStatement) astNode);
			} else if(astNode instanceof Name) {
				handleName((Name) astNode);
			}
			return true;
		} catch(Exception e) {
			e.printStackTrace();
		}
		return false;
}

private void handleName(Name name) {
	if(!isVariableReferenceName(name)) {
		return;
	}
	Tag tag = TagManager.getUniqueTag(name, "scope");
	
	if(tag != null) {
		String scopeString = tag.getValue().toString();
		Tag t = TagManager.getUniqueTag(name, "isClosed");
	
		if(!scopeNames.isEmpty() && fnciv.isOnClosurePath(funs.peek())) { //  )
			if(scopeString.startsWith("up") || (scopeString.equals("local") && ((t != null) || forcedClosed.contains(scopeNames.peek() + "." + name.getString()))) ) {
				//Name target = name(scopeNames.peek() + addLookup(name));
				if(name.getParent() instanceof InfixExpression) {
					InfixExpression ie = (InfixExpression) name.getParent();
					if(ASTFactory.isAssignmentType(ie.getOperator())) {
						AstNode parent = ie.getParent();
						Name target = name(fnNames.get(fnNames.size()-2) + "_scope." + name.getString());
						Assignment assign = ASTFactory.assign(target, ie.getLeft());
						InfixExpression comma = ASTFactory.comma(ie, assign);
						//PropertyGet pg = get(target, name.getIdentifier());
						if(parent instanceof ExpressionStatement) {
							tx.replaceNode(parent, ie, comma);
						}
					}
				}
				if(name.getParent() instanceof UnaryExpression) {
					UnaryExpression ue = (UnaryExpression) name.getParent();
					int op = ue.getOperator();
					if((op == Token.INC) || (op == Token.DEC)) {
						if(ue.isPrefix()) {
							AstNode parent = ue.getParent();
							Name target = name(fnNames.get(fnNames.size()-2) + "_scope." + name.getString());
							Assignment assign = ASTFactory.assign(target, ue.getOperand());
							InfixExpression comma = ASTFactory.comma(ue, assign);
							//PropertyGet pg = get(target, name.getIdentifier());
							tx.replaceNode(parent, ue, ASTFactory.paren(comma));
						} else {
							AstNode parent = ue.getParent();
							Name target = name(fnNames.get(fnNames.size()-2) + "_scope." + name.getString());
							Assignment assign = ASTFactory.assign(target, ue.getOperand());
							Assignment tmpAssign = ASTFactory.assign(name("__tmp"), ue);
							InfixExpression comma = ASTFactory.comma(paren(tmpAssign), ASTFactory.comma(paren(assign), name("__tmp")));
							//PropertyGet pg = get(target, name.getIdentifier());
							tx.replaceNode(parent, ue, ASTFactory.paren(comma));
						}
					}
				}
			}
		} 
	}
}

	
	private boolean handleReturnStatement(ReturnStatement rs) {
		if(!funs.isEmpty()) {
			FunctionNode fn = funs.peek();
			FunctionNodeScopeInfo fnsi = fnciv.getInfo().get(fn);
			for(String name : fnsi.getClosureVariables()) {
				String fnName = fnNames.peek();
				ExpressionStatement var = exprStmt(ASTFactory.assign(ASTFactory.get(name(fnName+"_scope"), name), name(name)));
				//ExpressionStatement var = exprStmt(ASTFactory.defineProperty(name("arguments.callee"), name, name(name)));
				AstNode parent = rs.getParent();
				tx.insertBefore(parent, rs, var); 
			}
		}
		return true;
	}
	
	private String handleFunctionName(FunctionNode fn) {
		String fnName = fn.getName();
		String scopedName = fnName;
		if((fnName == null) || fnName.equals("") ) {
			fnName = FUNCTION_PREFIX + (fnCount++);
			scopedName = getScopedName() + "_" + fnName;
			fn.setFunctionName(name(scopedName));
		}
		fnNames.push(fnName);
		FunctionNodeScopeInfo info = fnciv.getInfo().get(fn);
		if(info.hasChildren()) {
			VariableDeclaration var = ASTFactory.var(fnName + "_scope", name("{}"));
			tx.beforeExecution(fn, exprStmt(var));
			if(fnNames.size() > 1) {
				Assignment assign = ASTFactory.assign(name(fnName + "_scope.__outerScope"), name(fnNames.get(fnNames.size()-2) + "_scope"));
				tx.beforeExecution(fn, exprStmt(assign));
			}
		}
		if(info.isOnClosurePath()) {
			if(fnNames.size() > 1) {
				String parentName = fnNames.elementAt(fnNames.size() -2);
				//FunctionCall assign = ASTFactory.defineProperty(name("arguments.callee"), "__parent", name(parentName + "_scope"));
				//Assignment assign = ASTFactory.assign(name("arguments.callee.__parent"), name(parentName + "_scope"));
				//tx.beforeExecution(fn, exprStmt(assign));
				/*assign = ASTFactory.assign(name(parentName + "_scope.__name"), ASTFactory.string(parentName + "_scope"));
				tx.beforeExecution(fn, exprStmt(assign));
				if(info.hasChildren()) {
					assign = ASTFactory.assign(name("arguments.callee.__scope"), ASTFactory.name(fnName + "_scope"));
					tx.beforeExecution(fn, exprStmt(assign));
				}*/
			}
		}
		return fnName;
	}
	
	private String getScopedName() {
		String retVal = "";
		for(String s : fnNames) {
			retVal += "_" + s;
		}
		return retVal;
	}
	
	private void recordInnerFunction(AstNode parent, FunctionNode fn) {
		if(!(parent instanceof AstRoot)) {
			innerFunctions.add(fn);
		}
	}
	
	
	private boolean handleFunctionNode(FunctionNode fn) {
		AstNode parent = fn.getParent();
		recordInnerFunction(parent, fn);
		ObjectProperty parentScope = null;
		if(!this.scopeNames.isEmpty()) {
			parentScope = prop(name(PARENT_SCOPE_NAME), name(scopeNames.peek()));
		} 
		String outerScope = null;
		if(!scopeNames.isEmpty()) {
			outerScope = scopeNames.peek();
		}
		String scopeName = SCOPE_NAME + (++count);
		scopeNames.push(scopeName);
		funs.push(fn);
		String fnName = handleFunctionName(fn);
	
		if(!(parent instanceof AstRoot)) {
			if(!(parent instanceof Block)) {
				if(funs.size() > 1) {
					FunctionCall call = ASTFactory.call(name("__define"), fn, ASTFactory.name(fnNames.get(fnNames.size()-2) + "_scope"));
					tx.replaceNode(parent, fn, call);
				}
			} else {
				if(funs.size() > 1) {
					tx.insertAfter(parent, fn, exprStmt(name(fnName + ".__parentScope = " + fnNames.get(fnNames.size()-2) + "_scope")));
				}
			}
		} 
		
		fn.getBody().visit(this);
		
		List<Node> newer = new LinkedList<Node>();
		FunctionNodeScopeInfo fnsi = fnciv.getInfo().get(fn);
		String funName = fnNames.peek();
		for(String name : fnsi.getClosureVariables()) {
			ExpressionStatement var = exprStmt(ASTFactory.assign(ASTFactory.get(name(funName+"_scope"), name), name(name)));
			newer.add(var);
		}
		tx.afterExecution(fn, newer);
		scopeNames.pop();
		fnNames.pop();
		funs.pop();
		
		return false;
	}

private void handleFunctionExpression(ObjectProperty parentScope, FunctionNode fn) {
		
		
		//InfixExpression fstComma = comma(assign, comma(odp, name(TEMP_NAME)));
		AstNode rvalue;
		ExpressionStatement callStatement = ASTFactory.parseStmt(ExpressionStatement.class, fn.getName() + ".clone()");
		
		if(fnciv.isOnClosurePath(fn)) {
			Assignment assign = assign(name(TEMP_NAME), callStatement.getExpression());
			ObjectLiteral ol = new ObjectLiteral();
			
			//FunctionCall odp = ASTFactory.call("Object.defineProperty", ASTFactory.name("_tmp"), ASTFactory.string("__closure__"), o2);
			FunctionCall odp = ASTFactory.defineProperty(ASTFactory.name("_tmp"), "__closure__", ol);
			FunctionNode parentFn = null;
			if(funs.size() > 1) {
				parentFn = funs.get(funs.size()-2);
			}
			
			if((parentFn != null) && (fnciv.isOnClosurePath(parentFn))) {
				if(parentScope != null) {
					ol.addElement(parentScope);
				}
				rvalue = comma(assign, comma(odp, name(TEMP_NAME)));
			} else {
				rvalue = comma(assign, comma(odp, name(TEMP_NAME)));
			}
		} else {
			rvalue = fn;//callStatement.getExpression();
		}
		tx.replaceNode(fn.getParent(), fn, paren(rvalue));
	}
}